Lazy Setup
The book has now been published and the content of this chapter has likely changed substanstially.Please see page 435 of xUnit Test Patterns for the latest information.
How do we cause the Shared Fixture to be built before the first test method that needs it?
We use Lazy Initialization of the fixture to create it in the first test that needs it.
Sketch Lazy Setup embedded from Lazy Setup.gif
Shared Fixtures (page X) are often used to speed up test execution by reducing the number of times a complex fixture needs to be created. Unfortunately, a test that depends on other tests to set up the fixture cannot be run by itself; it is a Lonely Test (see Erratic Test on page X)
We can avoid this problem by having each test use Lazy Setup to set up the fixture if it is not already set up.
How It Works
We use Lazy Initialization[SBPP] to construct the fixture in the first test that needs it and store a reference to the fixture in a class variable that every test can access. All the subsequently run tests discover that the fixture is already created and that they can reuse it thus avoiding the effort of constructing it.
When To Use It
We can use Lazy Setup whenever we need to create a Shared Fixture yet we still want to be able to run each test by itself. We can also use Lazy Setup instead of other techniques like Setup Decorator (page X) or SuiteFixture Setup (page X) if it is not crucial that the fixture is torn down. This would be the case when we are using a fixture that can be torn down by Garbage-Collected Teardown (page X) or when we are using Distinct Generated Values (see Generated Value on page X) for all database keys and we aren't worried about leaving extra records lying around after each test; Delta Assertions (page X) make the latter possible.
The biggest detracting factor for Lazy Setup is the fact that while it is easy to discover that we are the first test and we need to construct the fixture, it is difficult to determine that we are the last test and that the fixture should be destroyed. Most members of the xUnit family of Test Automation Frameworks (page X) do not provide any way to determine this other than by using a Setup Decorator for the entire test suite. A few members of the xUnit family support SuiteFixture Setup (NUnit, VbUnit and JUnit 4.0 and newer to name a few) which provides setUp/tearDown "book ends" for a Testcase Class (page X). Unfortunately, this won't help us if we are writing our tests in Ruby, Python or PLSQL!
Some IDEs and Test Runners (page X) automatically reload our classes every time the test suite is run. This causes the original class variable to go out of scope and the fixture will be garbage-collected before the new version of the class is run. In these cases there is no negative consequence of using Lazy Setup.
A Prebuilt Fixture (page X) is another alternative to setting up the Shared Fixture for each test run but this can lead to Unrepeatable Tests (see Erratic Test) if the fixture is corrupted by some of the tests.
Implementation Notes
Since Lazy Setup only makes sense with Shared Fixtures, all the baggage that comes with Shared Fixtures comes with Lazy Setup.
Normally, Lazy Setup is used to build a Shared Fixture to be used by a single Testcase Class. The reference to the fixture is held in a class variable. Things get a bit trickier if we want to share the fixture across several Testcase Classes. We could move both the Lazy Initialization logic and the class variable to a Testcase Superclass (page X) but only if our language supports inheritance of class variables. The other alternative is to move the logic and variables to a Test Helper (page X).
I suppose we could use an approach like reference-counting as a way to know whether all the Test Methods (page X) have run. The challenge would be to know how many Testcase Objects (page X) are in the Test Suite Object (page X) so that we can compare it with the number of times the tearDown method has been called. I have never seen anyone do this so I won't call it a pattern! Adding logic to the Test Runner to invoke a tearDown method at the Test Suite Object level would amount to implementing SuiteFixture Setup.
Motivating Example
In this example, we have been building a new fixture for each Testcase Object.
public void testGetFlightsByFromAirport_OneOutboundFlight() throws Exception { setupStandardAirportsAndFlights(); FlightDto outboundFlight = findOneOutboundFlight(); // Exercise System List flightsAtOrigin = facade.getFlightsByOriginAirport( outboundFlight.getOriginAirportId()); // Verify Outcome assertOnly1FlightInDtoList( "Flights at origin", outboundFlight, flightsAtOrigin); } public void testGetFlightsByFromAirport_TwoOutboundFlights() throws Exception { setupStandardAirportsAndFlights(); FlightDto[] outboundFlights = findTwoOutboundFlightsFromOneAirport(); // Exercise System List flightsAtOrigin = facade.getFlightsByOriginAirport( outboundFlights[0].getOriginAirportId()); // Verify Outcome assertExactly2FlightsInDtoList( "Flights at origin", outboundFlights, flightsAtOrigin); } Example StandardTestFixture embedded from java/com/clrstream/ex6/services/test/FlightManagementFacadeTest.java
These tests are slow because creating the airports and flights involves a database. We can try refactoring these tests to set up the fixture in the setUp method (Implicit Setup (page X)):
protected void setUp() throws Exception { facade = new FlightMgmtFacadeImpl(); helper = new FlightManagementTestHelper(); setupStandardAirportsAndFlights(); oneOutboundFlight = findOneOutboundFlight(); } protected void tearDown() throws Exception { removeStandardAirportsAndFlights(); } public void testGetFlightsByOriginAirport_NoFlights_td() throws Exception { // Fixture setup BigDecimal outboundAirport = createTestAirport("1OF"); try { // Exercise System List flightsAtDestination1 = facade.getFlightsByOriginAirport(outboundAirport); // Verify Outcome assertEquals(0,flightsAtDestination1.size()); } finally { facade.removeAirport(outboundAirport); } } public void testGetFlightsByFromAirport_OneOutboundFlight() throws Exception { // Exercise System List flightsAtOrigin = facade.getFlightsByOriginAirport( oneOutboundFlight.getOriginAirportId()); // Verify Outcome assertOnly1FlightInDtoList( "Flights at origin", oneOutboundFlight, flightsAtOrigin); } public void testGetFlightsByFromAirport_TwoOutboundFlights() throws Exception { FlightDto[] outboundFlights = findTwoOutboundFlightsFromOneAirport(); // Exercise System List flightsAtOrigin = facade.getFlightsByOriginAirport( outboundFlights[0].getOriginAirportId()); // Verify Outcome assertExactly2FlightsInDtoList( "Flights at origin", outboundFlights, flightsAtOrigin); } Example ImplicitTestFixtureSetup embedded from java/com/clrstream/ex6/services/test/ImplicitFlightManagementFacadeTest.java
This doesn't speed up our tests one bit because the Test Automation Framework calls the setUp and tearDown methods for each Testcase Object. All we have done is moved the code. We need to find a way to set up the fixture only once per test run.
Refactoring Notes
We can reduce the number of times we set up the fixture by converting this test to Lazy Setup. Because we already have the fixture set up being done in the setUp method we need only insert the Lazy Initialization logic into the setUp method so that only the first test will cause it to be run. We must not forget to remove the tearDown logic as it will render the Lazy Initialization logic useless if it removes the fixture after each Test Method has run! Sorry, but there is nowhere that we can move this logic to so that it will be run after the last Test Method has completed if our xUnit family member doesn't support SuiteFixture Setup.
Example: Lazy Setup
Here is the same test refactored to use Lazy Setup.
protected void setUp() throws Exception { if (sharedFixtureInitialized) { return; } facade = new FlightMgmtFacadeImpl(); setupStandardAirportsAndFlights(); sharedFixtureInitialized = true; } protected void tearDown() throws Exception { // Cannot delete any objects because we don't know // whether or not this is the last test } Example LazyFixtureInitialization embedded from java/com/clrstream/ex6/services/test/SharedFixtureFlightManagementFacadeTest.java
Note that while there is a tearDown method on AirportFixture, there is no way to know when to call it! That's the main consequence of using Lazy Setup. Since the variables are static, they will not go out of scope so the fixture will not be garbage collected until the class is unloaded or reloaded.
The tests are unchanged from the Implicit Setup version:
public void testGetFlightsByFromAirport_OneOutboundFlight() throws Exception { FlightDto outboundFlight = findOneOutboundFlight(); // Exercise System List flightsAtOrigin = facade.getFlightsByOriginAirport( outboundFlight.getOriginAirportId()); // Verify Outcome assertOnly1FlightInDtoList( "Flights at origin", outboundFlight, flightsAtOrigin); } public void testGetFlightsByFromAirport_TwoOutboundFlights() throws Exception { FlightDto[] outboundFlights = findTwoOutboundFlightsFromOneAirport(); // Exercise System List flightsAtOrigin = facade.getFlightsByOriginAirport( outboundFlights[0].getOriginAirportId()); // Verify Outcome assertExactly2FlightsInDtoList( "Flights at origin", outboundFlights, flightsAtOrigin); } Example SharedTestFixture embedded from java/com/clrstream/ex6/services/test/SharedFixtureFlightManagementFacadeTest.java
Copyright © 2003-2008 Gerard Meszaros all rights reserved